/*******************************************************************************
 * Copyright (c) 2015, Institute for Pervasive Computing, ETH Zurich.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the Institute nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * This file is part of the Copper (Cu) CoAP user-agent.
 *******************************************************************************/
/**
 * \file   Implementation of RFC 7252
 *
 * \author Matthias Kovatsch <kovatsch@inf.ethz.ch>
 */

// Constants
////////////////////////////////////////////////////////////////////////////////
export class Copper {
    static utf8 = false;

    static get VERSION() { return 1; }
    static get DEFAULT_PORT() { return 5683; }
    static get RESPONSE_TIMEOUT() { return 2000; } // ms
    static get RESPONSE_RANDOM_FACTOR() { return 1.5; } // ms
    static get MAX_RETRANSMIT() { return 4; }
    static get ETAG_LENGTH() { return 8; }
    static get TOKEN_LENGTH() { return 8; }

    static get MSG_TYPE_CON() { return 0; }
    static get MSG_TYPE_NON() { return 1; }
    static get MSG_TYPE_ACK() { return 2; }
    static get MSG_TYPE_RST() { return 3; }

    static get OPTION_IF_MATCH() { return 1; }
    static get OPTION_URI_HOST() { return 3; }
    static get OPTION_ETAG() { return 4; }
    static get OPTION_IF_NONE_MATCH() { return 5; }
    static get OPTION_URI_PORT() { return 7; }
    static get OPTION_LOCATION_PATH() { return 8; }
    static get OPTION_URI_PATH() { return 11; }
    static get OPTION_CONTENT_FORMAT() { return 12; }
    static get OPTION_MAX_AGE() { return 14; }
    static get OPTION_URI_QUERY() { return 15; }
    static get OPTION_ACCEPT() { return 17; }
    static get OPTION_LOCATION_QUERY() { return 20; }
    static get OPTION_PROXY_URI() { return 35; }
    static get OPTION_PROXY_SCHEME() { return 39; }
    static get OPTION_SIZE1() { return 60; }

    static get OPTION_OBSERVE() { return 6; }

    static get OPTION_BLOCK2() { return 23; }
    static get OPTION_BLOCK1() { return 27; }
    static get OPTION_SIZE2() { return 28; }

    static get EMPTY() { return 0; }

    static get GET() { return 1; }
    static get POST() { return 2; }
    static get PUT() { return 3; }
    static get DELETE() { return 4; }

    static get CODE_2_01_CREATED() { return 65; }
    static get CODE_2_02_DELETED() { return 66; }
    static get CODE_2_03_VALID() { return 67; }
    static get CODE_2_04_CHANGED() { return 68; }
    static get CODE_2_05_CONTENT() { return 69; }
    static get CODE_2_31_CONTINUE() { return 95; }

    static get CODE_4_00_BAD_REQUEST() { return 128; }
    static get CODE_4_01_UNAUTHORIZED() { return 129; }
    static get CODE_4_02_BAD_OPTION() { return 130; }
    static get CODE_4_03_FORBIDDEN() { return 131; }
    static get CODE_4_04_NOT_FOUND() { return 132; }
    static get CODE_4_05_METHOD_NOT_ALLOWED() { return 133; }
    static get CODE_4_06_NOT_ACCEPTABLE() { return 134; }
    static get CODE_4_08_REQUEST_ENTITY_INCOMPLETE() { return 136; }
    static get CODE_4_12_PRECONDITION_FAILED() { return 140; }
    static get CODE_4_13_REQUEST_ENTITY_TOO_LARGE() { return 141; }
    static get CODE_4_15_UNSUPPORTED_MEDIA_TYPE() { return 143; }

    static get CODE_5_00_INTERNAL_SERVER_ERROR() { return 160; }
    static get CODE_5_01_NOT_IMPLEMENTED() { return 161; }
    static get CODE_5_02_BAD_GATEWAY() { return 162; }
    static get CODE_5_03_SERVICE_UNAVAILABLE() { return 163; }
    static get CODE_5_04_GATEWAY_TIMEOUT() { return 164; }
    static get CODE_5_05_PROXYING_NOT_SUPPORTED() { return 165; }

    static get CONTENT_TYPE_TEXT_PLAIN() { return 0; }
    static get CONTENT_TYPE_TEXT_XML() { return 1; }
    static get CONTENT_TYPE_TEXT_CSV() { return 2; }
    static get CONTENT_TYPE_TEXT_HTML() { return 3; }
    static get CONTENT_TYPE_IMAGE_GIF() { return 21; } // 03
    static get CONTENT_TYPE_IMAGE_JPEG() { return 22; } // 03
    static get CONTENT_TYPE_IMAGE_PNG() { return 23; } // 03
    static get CONTENT_TYPE_IMAGE_TIFF() { return 24; } // 03
    static get CONTENT_TYPE_AUDIO_RAW() { return 25; } // 03
    static get CONTENT_TYPE_VIDEO_RAW() { return 26; } // 03
    static get CONTENT_TYPE_APPLICATION_LINK_FORMAT() { return 40; }
    static get CONTENT_TYPE_APPLICATION_XML() { return 41; }
    static get CONTENT_TYPE_APPLICATION_OCTET_STREAM() { return 42; }
    static get CONTENT_TYPE_APPLICATION_RDF_XML() { return 43; }
    static get CONTENT_TYPE_APPLICATION_SOAP_XML() { return 44; }
    static get CONTENT_TYPE_APPLICATION_ATOM_XML() { return 45; }
    static get CONTENT_TYPE_APPLICATION_XMPP_XML() { return 46; }
    static get CONTENT_TYPE_APPLICATION_EXI() { return 47; }
    static get CONTENT_TYPE_APPLICATION_FASTINFOSET() { return 48; } // 04
    static get CONTENT_TYPE_APPLICATION_SOAP_FASTINFOSET() { return 49; } // 04
    static get CONTENT_TYPE_APPLICATION_JSON() { return 50; } // 04
    static get CONTENT_TYPE_APPLICATION_X_OBIX_BINARY() { return 51; }
    static get CONTENT_TYPE_APPLICATION_CBOR() { return 60; } // 04
    static get CONTENT_TYPE_APPLICATION_VND_OMA_LWM2M_TEXT() { return 1541; }
    static get CONTENT_TYPE_APPLICATION_VND_OMA_LWM2M_TLV() { return 1542; }
    static get CONTENT_TYPE_APPLICATION_VND_OMA_LWM2M_JSON() { return 1543; }
    static get CONTENT_TYPE_APPLICATION_VND_OMA_LWM2M_OPAQUE() { return 1544; }

    static get CONTENT_TYPE_APPLICATION_THING_DESCRIPTION_JSON() { return 65200; }
    static get CONTENT_TYPE_APPLICATION_BULLETIN_BOARD_JSON() { return 65201; }
    static get CONTENT_TYPE_APPLICATION_LIGHTING_CONFIG_JSON() { return 65202; }
    static get CONTENT_TYPE_APPLICATION_LIGHTING_JSON() { return 65203; }

    static get WELL_KNOWN_RESOURCES() { return '/.well-known/core'; }

    //Registries
    ////////////////////////////////////////////////////////////////////////////////

    static getCodeName(code) {
        switch (parseInt(code)) {
            // empty
            case 0: return 'EMPTY';
            // methods
            case Copper.GET: return 'GET';
            case Copper.POST: return 'POST';
            case Copper.PUT: return 'PUT';
            case Copper.DELETE: return 'DELETE';
            // response codes
            case Copper.CODE_2_01_CREATED: return '2.01 Created';
            case Copper.CODE_2_02_DELETED: return '2.02 Deleted';
            case Copper.CODE_2_03_VALID: return '2.03 Valid';
            case Copper.CODE_2_04_CHANGED: return '2.04 Changed';
            case Copper.CODE_2_05_CONTENT: return '2.05 Content';
            case Copper.CODE_2_31_CONTINUE: return '2.31 Continue';
            case Copper.CODE_4_00_BAD_REQUEST: return '4.00 Bad Request';
            case Copper.CODE_4_01_UNAUTHORIZED: return '4.01 Unauthorized';
            case Copper.CODE_4_02_BAD_OPTION: return '4.02 Bad Option';
            case Copper.CODE_4_03_FORBIDDEN: return '4.03 Forbidden';
            case Copper.CODE_4_04_NOT_FOUND: return '4.04 Not Found';
            case Copper.CODE_4_05_METHOD_NOT_ALLOWED: return '4.05 Method Not Allowed';
            case Copper.CODE_4_06_NOT_ACCEPTABLE: return '4.06 Not Acceptable';
            case Copper.CODE_4_08_REQUEST_ENTITY_INCOMPLETE: return '4.08 Request Entity Incomplete';
            case Copper.CODE_4_12_PRECONDITION_FAILED: return '4.12 Precondition Failed';
            case Copper.CODE_4_13_REQUEST_ENTITY_TOO_LARGE: return '4.13 Request Entity Too Large';
            case Copper.CODE_4_15_UNSUPPORTED_MEDIA_TYPE: return '4.15 Unsupported Content-Format';
            case Copper.CODE_5_00_INTERNAL_SERVER_ERROR: return '5.00 Internal Server Error';
            case Copper.CODE_5_01_NOT_IMPLEMENTED: return '5.01 Not Implemented';
            case Copper.CODE_5_02_BAD_GATEWAY: return '5.02 Bad Gateway';
            case Copper.CODE_5_03_SERVICE_UNAVAILABLE: return '5.03 Service Unavailable';
            case Copper.CODE_5_04_GATEWAY_TIMEOUT: return '5.04 Gateway Timeout';
            case Copper.CODE_5_05_PROXYING_NOT_SUPPORTED: return '5.05 Proxying Not Supported';
            // ...
            default: return Math.floor(code/32)+'.'+(code % 32)+' Unknown by Copper';
        }
    }

    static getOptionName(number) {
        switch (parseInt(number)) {
            case Copper.OPTION_CONTENT_FORMAT: return 'Content-Format';
            case Copper.OPTION_MAX_AGE: return 'Max-Age';
            case Copper.OPTION_ACCEPT: return 'Accept';

            case Copper.OPTION_URI_HOST: return 'Uri-Host';
            case Copper.OPTION_URI_PORT: return 'Uri-Port';
            case Copper.OPTION_URI_PATH: return 'Uri-Path';
            case Copper.OPTION_URI_QUERY: return 'Uri-Query';

            case Copper.OPTION_LOCATION_PATH: return 'Location-Path';
            case Copper.OPTION_LOCATION_QUERY: return 'Location-Query';

            case Copper.OPTION_PROXY_URI: return 'Proxy-Uri';
            case Copper.OPTION_PROXY_SCHEME: return 'Proxy-Scheme';

            case Copper.OPTION_IF_MATCH: return 'If-Match';
            case Copper.OPTION_IF_NONE_MATCH: return 'If-None-Match';
            case Copper.OPTION_ETAG: return 'ETag';

            case Copper.OPTION_OBSERVE: return 'Observe';

            case Copper.OPTION_BLOCK2: return 'Block2';
            case Copper.OPTION_BLOCK1: return 'Block1';

            case Copper.OPTION_SIZE2: return 'Size2';
            case Copper.OPTION_SIZE1: return 'Size1';

            default: return 'Unknown '+number;
        }
    }

    static getContentFormatName(type) {
        switch (type) {
            case Copper.CONTENT_TYPE_TEXT_PLAIN: return 'text/plain'; break;
            case Copper.CONTENT_TYPE_TEXT_XML: return 'text/xml'; break;
            case Copper.CONTENT_TYPE_TEXT_CSV: return 'text/csv'; break;
            case Copper.CONTENT_TYPE_TEXT_HTML: return 'text/html'; break;
            case Copper.CONTENT_TYPE_IMAGE_GIF: return 'image/gif'; break;
            case Copper.CONTENT_TYPE_IMAGE_JPEG: return 'image/jpeg'; break;
            case Copper.CONTENT_TYPE_IMAGE_PNG: return 'image/png'; break;
            case Copper.CONTENT_TYPE_IMAGE_TIFF: return 'image/tiff'; break;
            case Copper.CONTENT_TYPE_AUDIO_RAW: return 'audio/raw'; break;
            case Copper.CONTENT_TYPE_VIDEO_RAW: return 'video/raw'; break;
            case Copper.CONTENT_TYPE_APPLICATION_LINK_FORMAT: return 'application/link-format'; break;
            case Copper.CONTENT_TYPE_APPLICATION_XML: return 'application/xml'; break;
            case Copper.CONTENT_TYPE_APPLICATION_OCTET_STREAM: return 'application/octet-stream'; break;
            case Copper.CONTENT_TYPE_APPLICATION_RDF_XML: return 'application/rdf+xml'; break;
            case Copper.CONTENT_TYPE_APPLICATION_SOAP_XML: return 'application/soap+xml'; break;
            case Copper.CONTENT_TYPE_APPLICATION_ATOM_XML: return 'application/atom+xml'; break;
            case Copper.CONTENT_TYPE_APPLICATION_XMPP_XML: return 'application/xmpp+xml'; break;
            case Copper.CONTENT_TYPE_APPLICATION_EXI: return 'application/exi'; break;
            case Copper.CONTENT_TYPE_APPLICATION_FASTINFOSET: return 'application/fastinfoset'; break;
            case Copper.CONTENT_TYPE_APPLICATION_SOAP_FASTINFOSET: return 'application/soap+fastinfoset'; break;
            case Copper.CONTENT_TYPE_APPLICATION_JSON: return 'application/json'; break;
            case Copper.CONTENT_TYPE_APPLICATION_X_OBIX_BINARY: return 'application/x-obix-binary'; break;
            case Copper.CONTENT_TYPE_APPLICATION_CBOR: return 'application/cbor'; break;
            case Copper.CONTENT_TYPE_APPLICATION_VND_OMA_LWM2M_TEXT: return 'application/vnd.oma.lwm2m+text'; break;
            case Copper.CONTENT_TYPE_APPLICATION_VND_OMA_LWM2M_TLV: return 'application/vnd.oma.lwm2m+tlv'; break;
            case Copper.CONTENT_TYPE_APPLICATION_VND_OMA_LWM2M_JSON: return 'application/vnd.oma.lwm2m+json'; break;
            case Copper.CONTENT_TYPE_APPLICATION_VND_OMA_LWM2M_OPAQUE: return 'application/vnd.oma.lwm2m+opaque'; break;
            default: return 'unknown/unknown';
        }
    }

    // CoAP RFC 7252 implementation
    ////////////////////////////////////////////////////////////////////////////////

    static serialize(message) {
        let byteArray = new Array();
        let tempByte = 0x00;

        // first byte: version, type, and token length
        tempByte  = (0x03 & Copper.VERSION) << 6; // using const for sending packets
        tempByte |= (0x03 & message.type) << 4;
        tempByte |= (0x0F & message.token.length);

        byteArray.push(tempByte);

        // second byte: method or response code
        byteArray.push(0xFF & message.code);

        // third and forth byte: message ID (MID)
        byteArray.push(0xFF & (message.mid >>> 8));
        byteArray.push(0xFF & message.mid);

        for (let i in message.token) {
            byteArray.push(0xFF & message.token[i]);
        }

        // options
        message.optionCount = 0;
        let optNumber = 0;

        for (let optTypeIt in message.options) {

            if (!Array.isArray(message.options[optTypeIt][1])) {
                continue;
            } else {

                //Copper.logEvent("SERIALIZE: Option "+Copper.getOptionName(optTypeIt));

                let splitOption = new Array();
                if (optTypeIt==Copper.OPTION_LOCATION_PATH ||
                    optTypeIt==Copper.OPTION_LOCATION_QUERY ||
                    optTypeIt==Copper.OPTION_URI_PATH ||
                    optTypeIt==Copper.OPTION_URI_QUERY) {

                    let separator = '/'; // 0x002F
                    if (optTypeIt==Copper.OPTION_LOCATION_QUERY || optTypeIt==Copper.OPTION_URI_QUERY) {
                        separator = '&'; // 0x0026
                    }

                    if (Copper.bytes2str(message.options[optTypeIt][1])!="") {
                        let splitString = Copper.bytes2str(message.options[optTypeIt][1]).split(separator);
                        for (let s in splitString) {
                            splitOption.push(Copper.str2bytes(splitString[s]));
                        }
                    }
                } else {
                    splitOption.push(message.options[optTypeIt][1]);
                }

                while ((opt = splitOption.shift())) {

                    let optDelta = optTypeIt - optNumber;

                    let delta = Copper.optionNibble(optDelta);
                    let len = Copper.optionNibble(opt.length);

                    byteArray.push(0xFF & (delta<<4 | len));

                    if (delta==13) {
                        byteArray.push(optDelta-13);
                    } else if (delta==14) {
                        byteArray.push( (optDelta-269)>>>8 );
                        byteArray.push( 0xFF & (optDelta-269) );
                    }
                    if (len==13) {
                        byteArray.push(opt.length-13);
                    } else if (len==14) {
                        byteArray.push( (opt.length)>>>8 );
                        byteArray.push( 0xFF & (opt.length-269) );
                    }

                    // add option value
                    for (let i in opt) byteArray.push(opt[i]);

                    message.optionCount++;

                    optNumber = optTypeIt;
                }
            }
        }

        // option terminator
        if (message.payload.length>0) {
            byteArray.push(0xFF);
        }

        // serialize as string
        //let packet = Copper.bytes2data(byteArray);

        // payload
        //packet += Copper.bytes2data(message.payload);

        // finished
        return message.payload == undefined ? byteArray : byteArray.concat(message.payload);
    }

    static parse(packet) {
        Copper.logEvent('PACKET (hex): ' + packet.map(function(x){return x.toString(16).toUpperCase();}));

        // first byte: version, type, and option count
        let tempByte = packet.shift();
        let tokenLength = parseInt(0x0F & tempByte);
        let version = 0xFF & ((tempByte & 0xC0) >>> 6);

        if (version != Copper.VERSION) {
            throw new Error('Cannot parse CoAP version '+version);
        }

        // create the message
        let code = packet.shift();
        var message = new CoapMessage( 0x03 & ((tempByte) >>> 4), code);

        // third and forth byte: message ID (MID)
        message.mid  = packet.shift() << 8;
        message.mid |= packet.shift();

        Copper.logEvent("PARSE: Token length = "+tokenLength);
        for (let i=0; i<tokenLength; ++i) {
            message.token.push(packet.shift());
        }
        Copper.logEvent("PARSE: Token = "+message.getToken());

        // read options
        let optNumber = 0;

        while (packet.length > 0) {

            tempByte = packet.shift();

            if (tempByte!=0xFF) {
                optDelta = ((0xF0 & tempByte) >>> 4);
                optLen = (0x0F & tempByte);

                if (optDelta==13) {
                    optDelta += packet.shift();
                } else if (optDelta==14) {
                    optDelta += 255;
                    optDelta += packet.shift()<<8;
                    optDelta += 0xFF & packet.shift();
                }
                if (optLen==13) {
                    optLen += packet.shift();
                } else if (optLen==14) {
                    optLen += 255;
                    optLen += packet.shift()<<8;
                    optLen += 0xFF & packet.shift();
                }

                optNumber += optDelta;

                let opt = new Array(0);

                for (let j=0; j<optLen; j++) {
                    opt.push(packet.shift());
                }

                Copper.logEvent('PARSE: Option '+Copper.getOptionName(optNumber) + " = " + opt);

                // parse Option into Array
                if (optNumber==Copper.OPTION_LOCATION_PATH ||
                    optNumber==Copper.OPTION_LOCATION_QUERY ||
                    optNumber==Copper.OPTION_URI_PATH ||
                    optNumber==Copper.OPTION_URI_QUERY) {

                    var separator = 0x002F; // /
                    if (optNumber==Copper.OPTION_LOCATION_QUERY || optNumber==Copper.OPTION_URI_QUERY) {
                        separator = 0x0026; // &
                    }

                    if (message.options[optNumber][0]>0) {
                        optLen += 1 + message.options[optNumber][0];
                        opt = message.options[optNumber][1].concat(separator).concat(opt);
                    }
                }

                message.options[optNumber] = new Array(optLen, opt);

            } else {
                message.payload = packet;
                break;
            }
        }

        return message;
    }


    //Protocol helper functions
    ////////////////////////////////////////////////////////////////////////////////

    static optionNibble(value) {
        if (value < 13) {
            return (0xFF & value);
        } else if (value <= 0xFF+13) {
            return 13;
        } else if (value <= 0xFFFF+269) {
            return 14;
        } else {
            throw new Error('Option delta/length larger than 65804 not allowed');
        }
    }

    isPowerOfTwo(i) {
        return ((i & (i-1))==0);
    }

    static leadingZero(num, len) {
        if (!len) len = 2;
        num = ''+num;
        while (num.length<len) num = '0'+num;
        return num;
    }

    //for the string-oriented socket interface
    static bytes2data(b) {
        var str = '';
        for (let i in b) {
            str += String.fromCharCode(b[i] & 0xFF);
        }
        return str;
    }

    static data2bytes(data) {
        var b = new Array(0);
        for (let i=0; i<data.length; i++) {
            b.push(0xFF & data.charCodeAt(i));
        }
        return b;
    }

    static str2bytes(str) {

        let b = new Array(0);

        if (str!=null) {
            for (let i=0; i<str.length; i++) {
                let c = str.charCodeAt(i);

                if (c < 128 || !this.utf8) {
                    b.push(0xFF & c);
                } else if((c > 127) && (c < 2048)) {
                    b.push(0xFF & ((c >> 6) | 192));
                    b.push(0xFF & ((c & 63) | 128));
                } else {
                    b.push(0xFF & ((c >> 12) | 224));
                    b.push(0xFF & (((c >> 6) & 63) | 128));
                    b.push(0xFF & ((c & 63) | 128));
                }
            }
        }

        return b;
    }

    static bytes2str(b) {

        let str = '';
        for (let i=0; i<b.length; ++i) {

            let c = b[i] & 0xFF;

            if (c == 10 || (c >= 32 && c < 127)) {
                str += String.fromCharCode(c);
            } else if(this.utf8 && c >= 192 && c < 224 && (i+1 < b.length) && (b[i+1] & 0xc0) == 0x80) {
                let c1 = c & 0x1f;
                let c2 = b[i+1] & 0x3F;
                str += String.fromCharCode((c1 << 6) | c2);
                i += 1;
            } else if (this.utf8 && c >= 224 && c < 240 && (i+2 < b.length) && (b[i+1] & 0xc0) == 0x80 && (b[i+2] & 0xc0) == 0x80) {
                let c1 = c & 0x0f;
                let c2 = b[i+1] & 0x3F;
                let c3 = b[i+2] & 0x3F;
                str += String.fromCharCode((c1 << 12) | (c2 << 6) | c3);
                i += 2;
            } else if (this.utf8 && c >= 240 && i+3 < b.length) {
                Copper.logEvent('4-byte UTF-8');
                str += String.fromCharCode(0xFFFD); // char '�'
                i += 3;
            } else if (this.utf8 && c >= 128) {
                Copper.logEvent('Incomplete UTF-8 encoding');
                str += String.fromCharCode(0xFFFD); // char '�'
            } else {
                if (c < 32)
                    str += String.fromCharCode(0x2400 + c); // replacement character block
                else
                    str += String.fromCharCode(0xFFFD); // char '�'
    //			str += "\\x" + (c < 16 ? "0" : "") + c.toString(16);
            }
        }
        return str;
    }

    static int2bytes(i) {
        var b = new Array(0);
        while (i>0) {
            b.unshift(0xFF & i);
            i >>>= 8;
        }
        return b;
    }

    static bytes2int(b) {
        var i = 0;
        for (let k in b) {
            i = (i << 8) | b[k];
        }
        //convert to unsigned int
        return i>>>0;
    }

    static hex2bytes(h) {
        var b = new Array();
        for (let i=h.length-2; i>0; i-=2) {
            b.unshift(parseInt(('0x'+h.substr(i,2)).replace(/xx/, 'x')));
        }
        return b;
    }

    static bytes2hex(b) {

        if (!b || !Array.isArray(b) || b.length==0) {
            return 'empty';
        }

        var hex = '0x';
        for (let k in b) {
            if (b[k]!==undefined) {
                hex += this.leadingZero(b[k].toString(16).toUpperCase());
            } else {
                hex += '--';
            }
        }

        return hex;
    };

    static str2hex(s) {
        var temp;
        if (s.substr(0,2)=='0x') {
            temp = hex2bytes(s);
        } else {
            temp = str2bytes(s);
        }

        return bytes2hex(temp);
    }

    static float2bytes(value) {
        var bytes = 0;
        switch (value) {
            case Number.POSITIVE_INFINITY: bytes = 0x7F800000; break;
            case Number.NEGATIVE_INFINITY: bytes = 0xFF800000; break;
            case +0.0: bytes = 0x40000000; break;
            case -0.0: bytes = 0xC0000000; break;
            default:
                if (Number.isNaN(value)) { bytes = 0x7FC00000; break; }

                if (value <= -0.0) {
                    bytes = 0x80000000;
                    value = -value;
                }

                var exponent = Math.floor(Math.log(value) / Math.log(2));
                var significand = ((value / Math.pow(2, exponent)) * 0x00800000) | 0;

                exponent += 127;
                if (exponent >= 0xFF) {
                    exponent = 0xFF;
                    significand = 0;
                } else if (exponent < 0) exponent = 0;

                bytes = bytes | (exponent << 23);
                bytes = bytes | (significand & ~(-1 << 23));
            break;
        }
        return bytes;
    }

    static double2bytes(value) {

        var hiWord = 0, loWord = 0;
        switch (value) {
            case Number.POSITIVE_INFINITY: hiWord = 0x7FF00000; break;
            case Number.NEGATIVE_INFINITY: hiWord = 0xFFF00000; break;
            case +0.0: hiWord = 0x40000000; break;
            case -0.0: hiWord = 0xC0000000; break;
            default:
                if (Number.isNaN(value)) { hiWord = 0x7FF80000; break; }

                if (value <= -0.0) {
                    hiWord = 0x80000000;
                    value = -value;
                }

                var exponent = Math.floor(Math.log(value) / Math.log(2));
                var significand = Math.floor((value / Math.pow(2, exponent)) * Math.pow(2, 52));

                loWord = significand & 0xFFFFFFFF;
                significand /= Math.pow(2, 32);

                exponent += 1023;
                if (exponent >= 0x7FF) {
                    exponent = 0x7FF;
                    significand = 0;
                } else if (exponent < 0) exponent = 0;

                hiWord = hiWord | (exponent << 20);
                hiWord = hiWord | (significand & ~(-1 << 20));
            break;
        }

        let bytes = new Array(0);

        bytes.unshift( loWord>>24 & 0xFF );
        bytes.unshift( loWord>>16 & 0xFF );
        bytes.unshift( loWord>>8 & 0xFF );
        bytes.unshift( loWord>>0 & 0xFF );
        bytes.unshift( hiWord>>24 & 0xFF );
        bytes.unshift( hiWord>>16 & 0xFF );
        bytes.unshift( hiWord>>8 & 0xFF );
        bytes.unshift( hiWord>>0 & 0xFF );

        return bytes;
    }

    static logEvent(log) {
        console.log("Copper", log);
    }
}


export class CoapMessage
{

    //create a request/ack, received responses use parse()
    constructor(type, code, uri, pl) {

        this.version = Copper.VERSION;

        this.options = new Object();
        this.options[Copper.OPTION_CONTENT_FORMAT] = new Array(0, null);
        this.options[Copper.OPTION_MAX_AGE] = new Array(0, null);
        this.options[Copper.OPTION_PROXY_URI] = new Array(0, null);
        this.options[Copper.OPTION_PROXY_SCHEME] = new Array(0, null);
        this.options[Copper.OPTION_ETAG] = new Array(0, null);
        this.options[Copper.OPTION_URI_HOST] = new Array(0, null);
        this.options[Copper.OPTION_LOCATION_PATH] = new Array(0, null);
        this.options[Copper.OPTION_URI_PORT] = new Array(0, null);
        this.options[Copper.OPTION_LOCATION_QUERY] = new Array(0, null);
        this.options[Copper.OPTION_URI_PATH] = new Array(0, null);
        this.options[Copper.OPTION_OBSERVE] = new Array(0, null);
        this.options[Copper.OPTION_ACCEPT] = new Array(0, null);
        this.options[Copper.OPTION_IF_MATCH] = new Array(0, null);
        this.options[Copper.OPTION_URI_QUERY] = new Array(0, null);
        this.options[Copper.OPTION_BLOCK2] = new Array(0, null);
        this.options[Copper.OPTION_BLOCK1] = new Array(0, null);
        this.options[Copper.OPTION_SIZE2] = new Array(0, null);
        this.options[Copper.OPTION_SIZE1] = new Array(0, null);
        this.options[Copper.OPTION_IF_NONE_MATCH] = new Array(0, null);

        this.type = (type!==undefined) ? type : Copper.MSG_TYPE_CON;
        this.code = (code!==undefined) ? code : Copper.EMPTY;

        this.mid = -1;
        this.retries = 0;
        this.token = new Array(0);

        if (uri!==undefined) this.setUri( uri );
        if (pl!==undefined) {
            this.setPayload( pl );
        } else {
            this.payload = new Array(0);
        }

        return this;
    }

    // message summary (e.g., for info/debug dumps)
     getSummary() {
        let ret = '';
        ret += ' Type: '+this.getType(true);
        ret += '\n Code: '+this.getCode(true);
        ret += '\n Message ID: '+this.getMID();
        ret += '\n Token: '+this.getToken();
        if (Object.keys(this.options).length > 0) {
            ret += '\n Options: '+this.getOptions(true);
        }
        if (this.getPayload().length > 0) {
            ret += '\n Payload: '+this.getPayload().length+' bytes';
            if (this.isPrintable(this.getContentFormat())) {
                ret += '\n'+Copper.bytes2str(this.getPayload());
            }
        }

        return ret;
    }

    // readable type
    getType(readable) {
        if (readable) {
            switch (parseInt(this.type)) {
                case Copper.MSG_TYPE_CON: return 'CON';
                case Copper.MSG_TYPE_NON: return 'NON';
                case Copper.MSG_TYPE_ACK: return 'ACK';
                case Copper.MSG_TYPE_RST: return 'RST';
                default: return 'unknown ('+this.type+')';
            }
        } else {
            return parseInt(this.type);
        }
    }

    setType(type) {
        this.type = type;
    }

    isConfirmable() {
        return this.type==Copper.MSG_TYPE_CON;
    }

    getOptionCount() {
        return this.optionCount;
    }

    getCode(readable) {
        if (readable) {
            return Copper.getCodeName(this.code);
        } else {
            return parseInt(this.code);
        }
    }

    setCode(code) {
        this.code = code;
    }

    isRequest() {
        return this.getCode()>=1 && this.getCode()<=31;
    }

    isResponse() {
        return this.getCode()>=64;
    }

    isSuccess() {
        return Math.floor(this.getCode() / 32) == 2;
    }

    isClientError() {
        return Math.floor(this.getCode() / 32) == 4;
    }

    isServerError() {
        return Math.floor(this.getCode() / 32) == 5;
    }

    getMID()  {
        return this.mid;
    }

    setMID (id) {
        this.mid = id;
    }


    getToken()  {
        // return token as string for easy use as object key
        return Copper.bytes2data(this.token);
    }

    setToken (token) {
        if (!token) {
            token = new Array(0);
        } else if (token=='empty') {
            token = new Array(0);
        } else if (!Array.isArray(token)) {
            if (token.substr(0,2)=='0x') {
                token = Copper.hex2bytes(token);
            } else {
                token = Copper.str2bytes(token);
            }
        }

        while (token.length > Copper.TOKEN_LENGTH) {
            token.pop();
            Copper.logEvent('WARNING: Token must be 1-'+Copper.TOKEN_LENGTH+' bytes; cutting to '+Copper.TOKEN_LENGTH+' bytes]');
        }

        delete this.token;
        this.token = token;
    }

    // readable options list
    getOptions (asString) {

        if (asString) {
            let ret = '';

            for (let o in this.options) {

                if (Array.isArray(this.options[o][1])) {

                    if (ret!='') ret += ', ';

                    let name = Copper.getOptionName(o);
                    let val = this.getOption(o);
                    let info = this.options[o][0];

                    switch (parseInt(o)) {
                        case Copper.OPTION_BLOCK2:
                            val = this.getBlock2Number();
                            val += '/'+this.getBlock2More();
                            val += '/'+this.getBlock2Size();
                            break;
                        case Copper.OPTION_BLOCK1:
                            val = this.getBlock1Number();
                            val += '/'+this.getBlock1More();
                            val += '/'+this.getBlock1Size();
                            break;
                    }

                    ret += name + ': ' + val;
                }
            }

            return ret;
        } else {
            let ret = new Array();

            for (let o in this.options) {
                if (Array.isArray(this.options[o][1])) {
                    var name = Copper.getOptionName(o);
                    var value = this.getOption(o);
                    var info = this.options[o][0]+' byte' + (this.options[o][0]!=1 ? 's' : '');

                    switch (parseInt(o)) {
                        case Copper.OPTION_URI_PATH:
                        case Copper.OPTION_LOCATION_PATH:
                            value = '/' + value;
                            break;
                        case Copper.OPTION_CONTENT_FORMAT:
                            info = value;
                            value = Copper.getContentFormatName(value);
                            break;
                        case Copper.OPTION_ACCEPT:
                            info = value;
                            value = Copper.getContentFormatName(value);
                            break;
                        case Copper.OPTION_IF_NONE_MATCH:
                            info = '';
                            value = 'Set';
                            break;
                        case Copper.OPTION_BLOCK2:
                            value = this.getBlock2Number();
                            if (this.getBlock2More()) value += '+';
                            value += ' ('+this.getBlock2Size()+' B/block)';
                            break;
                        case Copper.OPTION_BLOCK1:
                            value = this.getBlock1Number();
                            if (this.getBlock1More()) value += '+';
                            value += ' ('+this.getBlock1Size()+' B/block)';
                            break;
                    }

                    ret.push(new Array(name, value, info));
                }
            }

            return ret;
        }
    }

    // check if option is present
    isOption (optNumber) {
        if (this.options[optNumber]!==undefined && Array.isArray(this.options[optNumber][1])) {
            return true;
        } else {
            return false;
        }
    }

    // retrieve option
    getOptionLength (optNumber) {
        if (this.options[optNumber]!=null && this.options[optNumber][0]!=null) {
            return this.options[optNumber][0];
        } else {
            return -1;
        }
    }

    getOption (optNumber) {
        var opt = this.options[optNumber][1];

        // only set options are arrays
        if (!Array.isArray(opt)) {
            return null;
        }

        switch (parseInt(optNumber)) {
            // strings
            case Copper.OPTION_URI_HOST:
            case Copper.OPTION_URI_PATH:
            case Copper.OPTION_URI_QUERY:
            case Copper.OPTION_LOCATION_PATH:
            case Copper.OPTION_LOCATION_QUERY:
            case Copper.OPTION_PROXY_URI:
            case Copper.OPTION_PROXY_SCHEME:
                return Copper.bytes2str(opt);
                break;

            // integers
            case Copper.OPTION_URI_PORT:
            case Copper.OPTION_CONTENT_FORMAT:
            case Copper.OPTION_MAX_AGE:
            case Copper.OPTION_ACCEPT:
            case Copper.OPTION_IF_NONE_MATCH:
            case Copper.OPTION_OBSERVE:
            case Copper.OPTION_BLOCK2:
            case Copper.OPTION_BLOCK1:
            case Copper.OPTION_SIZE2:
            case Copper.OPTION_SIZE1:
                return Copper.bytes2int(opt);

            // byte arrays
            case Copper.OPTION_ETAG:
            case Copper.OPTION_IF_MATCH:
                return Copper.bytes2hex(opt);
            default:
                return Copper.bytes2hex(opt);
        }
        return null;
    }

    setOption (option, value) {
        switch (parseInt(option)) {
            // strings
            case Copper.OPTION_PROXY_URI:
            case Copper.OPTION_PROXY_SCHEME:
            case Copper.OPTION_LOCATION_PATH:
            case Copper.OPTION_LOCATION_QUERY:
            case Copper.OPTION_URI_HOST:
            case Copper.OPTION_URI_PATH:
            case Copper.OPTION_URI_QUERY:
                this.options[option][0] = value.length;
                this.options[option][1] = Copper.str2bytes(value);
                break;

            // byte arrays
            case Copper.OPTION_ETAG:
            case Copper.OPTION_IF_MATCH:
                this.options[option][0] = value.length;
                this.options[option][1] = value;
                break;

            // special arrays
            case -1:
                this.options[option][0] += 1;
                if (this.options[option][1]==null) this.options[option][1] = new Array(0);
                this.options[option][1][ this.options[option][0] ] = value;
                this.options[option][0] += 1;
                break;

            // integers
            case Copper.OPTION_CONTENT_FORMAT:
            case Copper.OPTION_MAX_AGE:
            case Copper.OPTION_URI_PORT:
            case Copper.OPTION_OBSERVE:
            case Copper.OPTION_ACCEPT:
            case Copper.OPTION_BLOCK2:
            case Copper.OPTION_BLOCK1:
            case Copper.OPTION_SIZE2:
            case Copper.OPTION_SIZE1:
            case Copper.OPTION_IF_NONE_MATCH:
                this.options[option][1] = Copper.int2bytes(value);
                this.options[option][0] = this.options[option][1].length;
                break;

            default:
                this.options[option] = new Array(value.length, value);
                Copper.logEvent("WARNING: Setting custom option '"+option+"': "+value);
        }
    }


    getContentFormat (readable) {
        var opt = this.getOption(Copper.OPTION_CONTENT_FORMAT); // integer

        if (opt==null) return null;

        if (readable) {
            return new Array(Copper.getContentFormatName(opt), opt);
        } else {
            return opt;
        }
    }

    setContentType (content) {
        if (content>0xFFFF) {
            Copper.logWarning('Ignoring Content-Format larger than 65535.');
        } else {
            this.setOption(Copper.OPTION_CONTENT_FORMAT, content);
        }
    }

    // Copper.OPTION_MAX_AGE:00+
    getMaxAge (readable) {
        var optLen = this.getOptionLength(Copper.OPTION_MAX_AGE);
        var opt = this.getOption(Copper.OPTION_MAX_AGE); // integer

        if (opt==null) return null;

        if (readable) {

            var ret = '';
            var time = opt;

            if (time==0) {
                ret += '0 ';
            } else {
                // split into weeks, days, hours, minutes, and seconds
                var s = time % 60;
                time = Math.floor(time/60);
                var m = time % 60;
                time = Math.floor(time/60);
                var h = time % 24;
                time = Math.floor(time/24);
                var d = time % 7;
                time = Math.floor(time/7);
                var w = time;

                var y = 0;
                if (w>104) var y = Math.round(1212424351/60.0/60.0/24.0/365.25*100.0)/100.0;

                // only print from largest to smallest given unit
                if (w) ret += w+'w ';
                if (d||(w&&(h||m||s))) ret += d+'d ';
                if (h||((w||d)&&(m||s))) ret += h+'h ';
                if (m||((w||d||h)&&s)) ret += m+'m ';
                if (s) ret += s+'s ';
                if (y) ret += '(~'+y+'y) ';
            }

            return new Array(ret.substring(0, ret.length-1), optLen+' byte(s)');
        } else {
            return opt;
        }
    }

    setMaxAge (age) {
        if (age>0xFFFFFFFF) {
            age = (0xFFFFFFFF & age);
            Copper.logWarning('Ignoring Max-Age larger than 2³²');
        }
        this.setOption(Copper.OPTION_MAX_AGE, age);
    }

    // Copper.OPTION_PROXY_URI:04+
    getProxyUri (readable) {
        return this.getOption(Copper.OPTION_PROXY_URI); // string
    }

    setProxyUri (proxy) {
        this.setOption(Copper.OPTION_PROXY_URI, proxy);
    }

    // Copper.OPTION_PROXY_URI:04+
    getProxyScheme (readable) {
        return this.getOption(Copper.OPTION_PROXY_SCHEME); // string
    }

    setProxyScheme (scheme) {
        this.setOption(Copper.OPTION_PROXY_SCHEME, scheme);
    }

    // Copper.OPTION_ETAG:00+
    getETag()  {
        return this.getOption(Copper.OPTION_ETAG); // byte array
    }

    setETag (tag) {

        if (!Array.isArray(tag)) {
            Copper.logEvent('INFO: Converting ETag to byte array');
            if (tag.substr(0,2)=='0x') {
                tag = Copper.hex2bytes(tag);
            } else {
                tag = Copper.str2bytes(tag);
            }
        }

        if (tag.length>Copper.ETAG_LENGTH) {
            Copper.logWarning('Reducing ETag from '+tag.length+' to '+Copper.ETAG_LENGTH+' bytes.');
            tag = tag.slice(0, Copper.ETAG_LENGTH-1);
        }

        this.setOption(Copper.OPTION_ETAG, tag);
    }


    // Copper.OPTION_URI_HOST:04+ / Copper.OPTION_URI_AUTH:03*renamed
    getUriHost()  {
        return this.getOption(Copper.OPTION_URI_HOST); // string
    }

    setUriHost (host) {
        this.setOption(Copper.OPTION_URI_HOST, host);
    }

    // Copper.OPTION_URI_PORT:04+
    getUriPort()  {
        return this.getOption(Copper.OPTION_URI_PORT); // int
    }

    setUriPort (port) {
        this.setOption(Copper.OPTION_URI_PORT, port);
    }

    // multiple Copper.OPTION_URI_PATH:04+ / Copper.OPTION_URI_PATH:03+
    getUriPath()  {
        // multiple Copper.OPTION_URI_PATH options should be concatinated during datagram parsing
        // TODO: maybe use a string array later

        return this.getOption(Copper.OPTION_URI_PATH); // string
    }

    // Copper.OPTION_URI_QUERY:03+
    getUriQuery()  {
        return this.getOption(Copper.OPTION_URI_QUERY); // string
    }

    // convenience function
    getUri (readable) {

        let host = this.getUriHost();
        let port = this.getUriPort();
        let path = this.getUriPath();
        let query = this.getUriQuery();

        let uri = '';
        let decoded = 0;
        let multiple = null;

        if (host) {
            uri += 'coap://' + host;
            ++decoded;
        }
        if (port) {
            uri += ':' + port;
            ++decoded;
        }
        if (path) {
            uri += '/' + path;
            multiple = path.match(/\//g);
            decoded += 1 + (multiple!=null ? multiple.length : 0);
        }
        if (query) {
            uri += '?' + query;
            multiple = query.match(/&/g);
            decoded += 1 + (multiple!=null ? multiple.length : 0);
        }

        if (decoded<=0) return null;

        if (readable) {
            return new Array('Uri', uri, decoded+(decoded==1 ? ' option' : ' options'));
        } else {
            return uri;
        }
    }

    setUri (hostname, pathname='', search='') {

        // var uri = document.createElementNS("http://www.w3.org/1999/xhtml","a");
/*
 * <a> tag as parser:
 *
 *		parser.protocol; // => "http:"
 *		parser.hostname; // => "example.com"
 *		parser.port; // => "3000"
 *		parser.pathname; // => "/pathname/"
 *		parser.search; // => "?search=test"
 *		parser.hash; // => "#hash"
 *		parser.host; // => "example.com:3000"
 */
        //uri.href = inputUri;

        if (hostname!='' // set
    //        && Copper.behavior.sendUriHost // enabled
            && !hostname.match(/^[0-9]{1,3}(\.[0-9]{1,3}){3}$/) // no IPv4 literal
            && !hostname.match(/^[0-9a-f]{0,4}(:?:[0-9a-f]{1,4})+$/i)) { // no IPv6 literal
            this.setOption(Copper.OPTION_URI_HOST, hostname);
        }
        if (pathname.length>1) {
            this.setOption(Copper.OPTION_URI_PATH, decodeURI(pathname.substr(1)));
        }
        if (search.length>1) {
            this.setOption(Copper.OPTION_URI_QUERY, search);
        }
    }

    // multiple Copper.OPTION_LOCATION_PATH:04+ / Copper.OPTION_LOCATION:03*renamed
    getLocationPath()  {
        // multiple Copper.OPTION_LOCATION_PATH options should be concatinated during datagram parsing
        // TODO: maybe use a string array later

        return this.getOption(Copper.OPTION_LOCATION_PATH); // string
    }

    setLocationPath (path) {
        while (path.charAt(0)=='/') path = path.substr(1);

        this.setOption(Copper.OPTION_LOCATION_PATH, path);
    }

    // Copper.OPTION_LOCATION_QUERY:05+
    getLocationQuery()  {
        return this.getOption(Copper.OPTION_LOCATION_QUERY); // string
    }

    setLocationQuery (query) {
        while (query.charAt(0)=='?') query = query.substr(1);

        this.setOption(Copper.OPTION_LOCATION_QUERY, query);
    }

    // convenience function
    getLocation (readable) {
        var optLen = this.getOptionLength(Copper.OPTION_LOCATION_PATH);
        var opt = this.getOption(Copper.OPTION_LOCATION_PATH); // string

        var optLen2 = 0;

        if (this.getOptionLength(Copper.OPTION_LOCATION_QUERY)) {
            opt += '?' + this.getOption(Copper.OPTION_LOCATION_QUERY);
            optLen2 = this.getOptionLength(Copper.OPTION_LOCATION_QUERY);
        }

        if (optLen+optLen2<=0) return null;

        if (readable) {
            var multiple = opt.match(/\/|&/g);
            var decoded = 1 + (multiple!=null ? multiple.length : 0) + (optLen2>0 ? 1 : 0);
            if (opt.charAt(0)!='/') opt = '/' + opt;
            return new Array(opt, decoded+(decoded==1 ? ' option' : ' options'));
        } else {
            if (opt) opt = '/'+opt;
            return opt;
        }
    }

    // Copper.OPTION_ACCEPT:07+
    getAccept()  {
        return this.getOption(Copper.OPTION_ACCEPT); // integer
    }

    setAccept (content) {
        if (content>0xFFFF) {
            Copper.logWarning('Ignoring Accept Content-Format larger than 65535.');
        } else {
            this.setOption(Copper.OPTION_ACCEPT, content);
        }
    }

    // Copper.OPTION_IF_MATCH:07+
    getIfMatch()  {
        return this.getOption(Copper.OPTION_IF_MATCH); // byte array
    }

    setIfMatch (tag) {
        if (!Array.isArray(tag)) {
            Copper.logEvent('INFO: Converting ETag to byte array');
            if (tag.substr(0,2)=='0x') {
                tag = Copper.hex2bytes(tag);
            } else {
                tag = Copper.str2bytes(tag);
            }
        }

        while (tag.length>Copper.ETAG_LENGTH) {
            Copper.logWarning('Reducing ETag from '+tag.length+' to '+Copper.ETAG_LENGTH+' bytes.');
            tag = tag.slice(0, Copper.ETAG_LENGTH-1);
        }
        this.setOption(Copper.OPTION_IF_MATCH, tag);
    }

    // Copper.OPTION_BLOCK2:06+ / Copper.OPTION_BLOCK:03+
    getBlock2 (readable) {
        var opt = this.getOption(Copper.OPTION_BLOCK2); // integer

        if (opt==null) return null;

        if (readable) {
            var ret = this.getBlock2Number();
            if (this.getBlock2More()) ret += '+';
            ret += ' ('+this.getBlock2Size()+' B/block)';

            return ret;
        } else {
            return opt;
        }
    }

    setBlock2 (num, size, more) {

        if (size!==undefined) {

            let block = num << 4;

            let szx = 0;

            // check for power of two and correct size
            if (!Copper.isPowerOfTwo(size)) {
                Copper.logEvent('INFO: Block2 size '+size+' not a power of two; using next smaller power.');
            }
            if (size<16) {
                size = 16;
                Copper.logEvent('INFO: Block2 size must be >=16; using 16.');
            }
            if (size>1024) {
                size = 1024;
                Copper.logEvent('INFO: Block2 size must be <=1024; using 1024.');
            }

            // size encoding
            size >>= 4;
            for (szx = 0; size; ++szx) size >>= 1;
            block |= (szx - 1);

            if (more!==undefined) {
                block |= more ? 0x08 : 0x00;
            }

            this.setOption(Copper.OPTION_BLOCK2, block);

        } else {
            this.setOption(Copper.OPTION_BLOCK2, num);
        }
    }

    // convenience functions for block option parts
    getBlock2Number()  {
        return (this.getBlock2() >> 4);
    }

    getBlock2Size()  {
        return (16 << (0x07 & this.getBlock2()));
    }

    getBlock2More()  {
        return (parseInt(0x08 & this.getBlock2())!=0) ? 1 : 0;
    }

    getBlock2Offset()  {
        return this.getBlock2Size() * (this.getBlock2Number() + 1);
    }


    // Copper.OPTION_BLOCK1:06+
    getBlock1 (readable) {
        var opt = this.getOption(Copper.OPTION_BLOCK1); // integer

        if (opt==null) return null;

        if (readable) {
            var ret = this.getBlock1Number();
            if (this.getBlock1More()) ret += '+';
            ret += ' ('+this.getBlock1Size()+' B/block)';

            return ret;
        } else {
            return opt;
        }
    }

    setBlock1 (num, size, more) {
        let block = num << 4;
        let szx = 0;

        // check for power of two and correct size
        if (!Copper.isPowerOfTwo(size)) {
            Copper.logEvent('INFO: Block1 size '+size+' not a power of two; using next smaller power.');
        }
        if (size<16) {
            size = 16;
            Copper.logEvent('INFO: Block1 size must be >=16; using 16.');
        }
        if (size>1024) {
            size = 1024;
            Copper.logEvent('INFO: Block1 size must be <=1024; using 1024.');
        }

        size >>= 4;
        for (szx = 0; size; ++szx) size >>= 1;
        block |= szx - 1;
        if (more) {
            block |= 0x08;
        }

        this.setOption(Copper.OPTION_BLOCK1, block);
    }

    // convenience functions for block option parts
    getBlock1Number()  {
        return (this.getBlock1() >> 4);
    }

    getBlock1Size()  {
        return (16 << (0x07 & this.getBlock1()));
    }

    getBlock1More()  {
        return (0x08 & this.getBlock1()) ? 1 : 0;
    }

    getBlock1Offset()  {
        return this.getBlock1Size() * (this.getBlock1Number() + 1);
    }


    // Copper.OPTION_SIZE2:18+ / Copper.OPTION_SIZE:09+
    getSize()  {
        return this.getOption(Copper.OPTION_SIZE2); // integer
    }

    setSize2 (num) {
        if (num>0xFFFFFFFF) {
            Copper.logWarning('Ignoring Size2 larger than 2³²');
        } else {
            this.setOption(Copper.OPTION_SIZE2, num);
        }
    }

    // Copper.OPTION_SIZE1:18+
    getSize1()  {
        return this.getOption(Copper.OPTION_SIZE1); // integer
    }

    setSize1 (num) {
        if (num>0xFFFFFFFF) {
            Copper.logWarning('Ignoring Size1 larger than 2³²');
        } else {
            this.setOption(Copper.OPTION_SIZE1, num);
        }
    }

    // Copper.OPTION_IF_NONE_MATCH:07+
    getIfNoneMatch()  {
        var opt = this.getOption(Copper.OPTION_IF_NONE_MATCH); // byte array

        return (opt==null ? null : 'none');
    }

    setIfNoneMatch()  {
        // only set option with length 0 (int=0)
        this.setOption(Copper.OPTION_IF_NONE_MATCH, 0);
    }

    // Copper.OPTION_SUB_LIFETIME:draft-ietf-core-observe-00*renamed
    getObserve()  {
        return this.getOption(Copper.OPTION_OBSERVE); // int
    }

    setObserve (num) {
        if (num> 0xFFFFFF) num = num % 0xFFFFFF;
        this.setOption(Copper.OPTION_OBSERVE, num);
    }

    setCustom (num, value) {
        if (Copper.getOptionName(num).match(/^Unknown/)) {
            if (value.substr(0,2)=='0x') {
                this.setOption(parseInt(num), Copper.hex2bytes(value));
            } else {
                this.setOption(parseInt(num), Copper.str2bytes(value));
            }
        } else {
            throw new Error('Cannot set '+Copper.getOptionName(num)+' as custom option');
        }
    }

    // payload functions
    getPayload()  {
        return this.payload;
    }

    getPayloadText()  {
        return Copper.bytes2str(this.payload);
    }

    setPayload (pl) {
        if (!Array.isArray(pl)) pl = Copper.str2bytes(pl);
        this.payload = pl;
    }

    appendPayload (pl) {
        this.payload = this.payload.concat(pl);
    }

    isPrintable (ct) {
        if (ct==null) ct = this.getContentFormat();

        switch (ct) {
            case Copper.CONTENT_TYPE_TEXT_PLAIN:
            case Copper.CONTENT_TYPE_TEXT_XML:
            case Copper.CONTENT_TYPE_TEXT_CSV:
            case Copper.CONTENT_TYPE_TEXT_HTML:
            case Copper.CONTENT_TYPE_APPLICATION_LINK_FORMAT:
            case Copper.CONTENT_TYPE_APPLICATION_XML:
            case Copper.CONTENT_TYPE_APPLICATION_RDF_XML:
            case Copper.CONTENT_TYPE_APPLICATION_SOAP_XML:
            case Copper.CONTENT_TYPE_APPLICATION_ATOM_XML:
            case Copper.CONTENT_TYPE_APPLICATION_XMPP_XML:
            case Copper.CONTENT_TYPE_APPLICATION_JSON:
            case Copper.CONTENT_TYPE_APPLICATION_CBOR:
            case Copper.CONTENT_TYPE_APPLICATION_VND_OMA_LWM2M_TEXT:
            case Copper.CONTENT_TYPE_APPLICATION_VND_OMA_LWM2M_JSON:
            case null:
                return true;

            case Copper.CONTENT_TYPE_IMAGE_GIF:
            case Copper.CONTENT_TYPE_IMAGE_JPEG:
            case Copper.CONTENT_TYPE_IMAGE_PNG:
            case Copper.CONTENT_TYPE_IMAGE_TIFF:
            case Copper.CONTENT_TYPE_AUDIO_RAW:
            case Copper.CONTENT_TYPE_VIDEO_RAW:
            case Copper.CONTENT_TYPE_APPLICATION_OCTET_STREAM:
            case Copper.CONTENT_TYPE_APPLICATION_EXI:
            case Copper.CONTENT_TYPE_APPLICATION_FASTINFOSET:
            case Copper.CONTENT_TYPE_APPLICATION_SOAP_FASTINFOSET:
            case Copper.CONTENT_TYPE_APPLICATION_X_OBIX_BINARY:
            case Copper.CONTENT_TYPE_APPLICATION_VND_OMA_LWM2M_TLV:
            case Copper.CONTENT_TYPE_APPLICATION_VND_OMA_LWM2M_OPAQUE:
            default:
                return false;
        }
    }


    // convert message into datagram bytes
    serialize()  {
        return Copper.serialize();
    }

    // convert datagram bytes into message
    parse (datagram) {
        this.parse(datagram);
    }

    getRetries()  {
        return this.retries;
    }

    incRetries()  {
        ++this.retries;
    }

    // maybe more arguments needed
    respond (code, payload, format) {
        this.reply = new CoapMessage(Copper.MSG_TYPE_ACK, code, null, payload);
        this.reply.setMID(this.getMID());
        this.reply.setToken(this.getToken());

        if (format) this.reply.setContentType(format);
    }

    getReply()  {
        return this.reply;
    }
}
